package com.example.s3demo.interceptor

import com.example.s3demo.config.SystemConfig
import com.example.s3demo.utils.ConvertOp
import com.example.s3demo.utils.StringUtil
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.servlet.HandlerInterceptor
import java.io.UnsupportedEncodingException
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec


@Component
class S3Interceptor(
    private val systemConfig: SystemConfig
): HandlerInterceptor {
    private val log = LoggerFactory.getLogger(S3Interceptor::class.java)


    @Throws(Exception::class)
    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        log.info("methods: {}, path: {}", request.method, request.requestURI)
        var flag = false
        var authorization: String = request.getHeader("Authorization")
        if (!StringUtil.isEmpty(authorization)) {
            flag = validAuthorizationHead(request, systemConfig.username, systemConfig.password)
        } else {
            authorization = request.getParameter("X-Amz-Credential")
            if (!StringUtil.isEmpty(authorization)) {
                flag = validAuthorizationUrl(request, systemConfig.username, systemConfig.password)
            }
        }
        if (!flag) {
            response.status = HttpStatus.UNAUTHORIZED.value()
        }
        return flag
    }

    @Throws(Exception::class)
    fun validAuthorizationHead(request: HttpServletRequest, accessKeyId: String, secretAccessKey: String): Boolean {
        val authorization: String = request.getHeader("Authorization")
        val requestDate: String = request.getHeader("x-amz-date")
        val contentHash: String = request.getHeader("x-amz-content-sha256")
        val httpMethod: String = request.method
        val uri: String = request.requestURI.split("\\?")[0]
        val queryString = ConvertOp.convert2String(request.queryString)
        //示例
        //AWS4-HMAC-SHA256 Credential=admin/20230530/us-east-1/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;host;x-amz-content-sha256;x-amz-date, Signature=6f50628a101b46264c7783937be0366762683e0d319830b1844643e40b3b0ed

        ///region authorization拆分
        val parts = authorization.trim { it <= ' ' }.split("\\,".toRegex()).dropLastWhile { it.isEmpty() }
            .toTypedArray()
        //第一部分-凭证范围
        val credential = parts[0].split("\\=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]
        val credentials = credential.split("\\/".toRegex()).dropLastWhile { it.isEmpty() }
            .toTypedArray()
        val accessKey = credentials[0]
        if (accessKeyId != accessKey) {
            return false
        }
        val date = credentials[1]
        val region = credentials[2]
        val service = credentials[3]
        val aws4Request = credentials[4]
        //第二部分-签名头中包含哪些字段
        val signedHeader = parts[1].split("\\=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]
        val signedHeaders = signedHeader.split("\\;".toRegex()).dropLastWhile { it.isEmpty() }
            .toTypedArray()
        //第三部分-生成的签名
        val signature = parts[2].split("\\=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]

        ///endregion

        ///region 待签名字符串
        var stringToSign: String = ""
        //签名由4部分组成
        //1-Algorithm – 用于创建规范请求的哈希的算法。对于 SHA-256，算法是 AWS4-HMAC-SHA256。
        stringToSign += "AWS4-HMAC-SHA256" + "\n"
        //2-RequestDateTime – 在凭证范围内使用的日期和时间。
        stringToSign += requestDate + "\n"
        //3-CredentialScope – 凭证范围。这会将生成的签名限制在指定的区域和服务范围内。该字符串采用以下格式：YYYYMMDD/region/service/aws4_request
        stringToSign += "$date/$region/$service/$aws4Request\n"
        //4-HashedCanonicalRequest – 规范请求的哈希。
        //<HTTPMethod>\n
        //<CanonicalURI>\n
        //<CanonicalQueryString>\n
        //<CanonicalHeaders>\n
        //<SignedHeaders>\n
        //<HashedPayload>
        var hashedCanonicalRequest = ""
        //4.1-HTTP Method
        hashedCanonicalRequest += httpMethod + "\n"
        //4.2-Canonical URI
        hashedCanonicalRequest += uri + "\n"
        //4.3-Canonical Query String
        if (!StringUtil.isEmpty(queryString)) {
            val queryStringMap = parseQueryParams(queryString)
            val keyList: List<String> = ArrayList(queryStringMap.keys)
            Collections.sort(keyList)
            val queryStringBuilder = StringBuilder("")
            for (key in keyList) {
                queryStringBuilder.append(key).append("=").append(queryStringMap[key]).append("&")
            }
            queryStringBuilder.deleteCharAt(queryStringBuilder.lastIndexOf("&"))

            hashedCanonicalRequest += queryStringBuilder.toString() + "\n"
        } else {
            hashedCanonicalRequest += queryString + "\n"
        }
        //4.4-Canonical Headers
        for (name in signedHeaders) {
            hashedCanonicalRequest += (name + ":" + request.getHeader(name)).toString() + "\n"
        }
        hashedCanonicalRequest += "\n"
        //4.5-Signed Headers
        hashedCanonicalRequest += signedHeader + "\n"
        //4.6-Hashed Payload
        hashedCanonicalRequest += contentHash
        stringToSign += doHex(hashedCanonicalRequest)

        ///endregion

        ///region 重新生成签名
        //计算签名的key
        val kSecret = "AWS4$secretAccessKey".toByteArray(charset("UTF8"))
        val kDate = doHmacSHA256(kSecret, date)
        val kRegion = doHmacSHA256(kDate, region)
        val kService = doHmacSHA256(kRegion, service)
        val signatureKey = doHmacSHA256(kService, aws4Request)
        //计算签名
        val authSignature = doHmacSHA256(signatureKey, stringToSign)
        //对签名编码处理
        val strHexSignature = doBytesToHex(authSignature)

        ///endregion
        if (signature == strHexSignature) {
            return true
        }
        return false
    }

    @Throws(Exception::class)
    fun validAuthorizationUrl(request: HttpServletRequest, accessKeyId: String, secretAccessKey: String): Boolean {
        val requestDate: String = request.getParameter("X-Amz-Date")
        val contentHash = "UNSIGNED-PAYLOAD"
        val httpMethod: String = request.method
        val uri: String = request.requestURI.split("\\?")[0]
        val queryString: String = ConvertOp.convert2String(request.queryString)

        //示例
        //"http://localhost:8001/s3/kkk/%E6%B1%9F%E5%AE%81%E8%B4%A2%E6%94%BF%E5%B1%80%E9%A1%B9%E7%9B%AE%E5%AF%B9%E6%8E%A5%E6%96%87%E6%A1%A3.docx?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230531T024715Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Credential=admin%2F20230531%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=038e2ea71073761aa0370215621599649e9228177c332a0a79f784b1a6d9ee39

        ///region 参数准备
        //第一部分-凭证范围
        val credential: String = request.getParameter("X-Amz-Credential")
        val credentials = credential.split("\\/".toRegex()).dropLastWhile { it.isEmpty() }
            .toTypedArray()
        val accessKey = credentials[0]
        if (accessKeyId != accessKey) {
            return false
        }
        val date = credentials[1]
        val region = credentials[2]
        val service = credentials[3]
        val aws4Request = credentials[4]
        //第二部分-签名头中包含哪些字段
        val signedHeader: String = request.getParameter("X-Amz-SignedHeaders")
        val signedHeaders = signedHeader.split("\\;".toRegex()).dropLastWhile { it.isEmpty() }
            .toTypedArray()
        //第三部分-生成的签名
        val signature: String = request.getParameter("X-Amz-Signature")

        ///endregion

        ///region 验证expire
        val expires: String = request.getParameter("X-Amz-Expires")
        val formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'")
        var startDate = LocalDateTime.parse(requestDate, formatter)
        val zoneId = ZoneId.systemDefault()
        val localDateTime = startDate.atZone(ZoneId.of("UTC")).withZoneSameInstant(zoneId)
        startDate = localDateTime.toLocalDateTime()
        val endDate = startDate.plusSeconds(ConvertOp.convert2Int(expires).toLong())
        if (endDate.isBefore(LocalDateTime.now())) {
            return false
        }

        ///endregion

        ///region 待签名字符串
        var stringToSign: String? = ""
        //签名由4部分组成
        //1-Algorithm – 用于创建规范请求的哈希的算法。对于 SHA-256，算法是 AWS4-HMAC-SHA256。
        stringToSign += "AWS4-HMAC-SHA256" + "\n"
        //2-RequestDateTime – 在凭证范围内使用的日期和时间。
        stringToSign += requestDate + "\n"
        //3-CredentialScope – 凭证范围。这会将生成的签名限制在指定的区域和服务范围内。该字符串采用以下格式：YYYYMMDD/region/service/aws4_request
        stringToSign += "$date/$region/$service/$aws4Request\n"
        //4-HashedCanonicalRequest – 规范请求的哈希。
        //<HTTPMethod>\n
        //<CanonicalURI>\n
        //<CanonicalQueryString>\n
        //<CanonicalHeaders>\n
        //<SignedHeaders>\n
        //<HashedPayload>
        var hashedCanonicalRequest = ""
        //4.1-HTTP Method
        hashedCanonicalRequest += httpMethod + "\n"
        //4.2-Canonical URI
        hashedCanonicalRequest += uri + "\n"
        //4.3-Canonical Query String
        if (!StringUtil.isEmpty(queryString)) {
            val queryStringMap = parseQueryParams(queryString)
            val keyList: List<String> = ArrayList(queryStringMap.keys)
            Collections.sort(keyList)
            val queryStringBuilder = StringBuilder("")
            for (key in keyList) {
                if (key != "X-Amz-Signature") {
                    queryStringBuilder.append(key).append("=").append(queryStringMap[key]).append("&")
                }
            }
            queryStringBuilder.deleteCharAt(queryStringBuilder.lastIndexOf("&"))

            hashedCanonicalRequest += queryStringBuilder.toString() + "\n"
        } else {
            hashedCanonicalRequest += queryString + "\n"
        }
        //4.4-Canonical Headers
        for (name in signedHeaders) {
            hashedCanonicalRequest += (name + ":" + request.getHeader(name)) + "\n"
        }
        hashedCanonicalRequest += "\n"
        //4.5-Signed Headers
        hashedCanonicalRequest += signedHeader + "\n"
        //4.6-Hashed Payload
        hashedCanonicalRequest += contentHash
        stringToSign += doHex(hashedCanonicalRequest)

        ///endregion

        ///region 重新生成签名
        //计算签名的key
        val kSecret = "AWS4$secretAccessKey".toByteArray(charset("UTF8"))
        val kDate = doHmacSHA256(kSecret, date)
        val kRegion = doHmacSHA256(kDate, region)
        val kService = doHmacSHA256(kRegion, service)
        val signatureKey = doHmacSHA256(kService, aws4Request)
        //计算签名
        val authSignature = doHmacSHA256(signatureKey, stringToSign)
        //对签名编码处理
        val strHexSignature = doBytesToHex(authSignature)

        ///endregion
        if (signature == strHexSignature) {
            return true
        }
        return false
    }

    private fun doHex(data: String): String? {
        val messageDigest: MessageDigest
        try {
            messageDigest = MessageDigest.getInstance("SHA-256")
            messageDigest.update(data.toByteArray(charset("UTF-8")))
            val digest = messageDigest.digest()
            return String.format("%064x", BigInteger(1, digest))
        } catch (e: NoSuchAlgorithmException) {
            e.printStackTrace()
        } catch (e: UnsupportedEncodingException) {
            e.printStackTrace()
        }
        return null
    }

    @Throws(Exception::class)
    private fun doHmacSHA256(key: ByteArray, data: String?): ByteArray {
        val algorithm = "HmacSHA256"
        val mac = Mac.getInstance(algorithm)
        mac.init(SecretKeySpec(key, algorithm))
        return mac.doFinal(data!!.toByteArray(charset("UTF8")))
    }

    private fun doBytesToHex(bytes: ByteArray): String {
        val hexChars = CharArray(bytes.size * 2)
        for (j in bytes.indices) {
            val v = bytes[j].toInt() and 0xFF
            hexChars[j * 2] = hexArray[v ushr 4]
            hexChars[j * 2 + 1] = hexArray[v and 0x0F]
        }
        return String(hexChars).lowercase(Locale.getDefault())
    }

    companion object {
        protected val hexArray: CharArray = "0123456789ABCDEF".toCharArray()

        fun parseQueryParams(queryString: String?): Map<String, String> {
            val queryParams: MutableMap<String, String> = HashMap()
            try {
                if (!queryString.isNullOrEmpty()) {
                    val queryParamsArray = queryString.split("\\&".toRegex()).dropLastWhile { it.isEmpty() }
                        .toTypedArray()

                    for (param in queryParamsArray) {
                        val keyValue = param.split("\\=".toRegex()).dropLastWhile { it.isEmpty() }
                            .toTypedArray()
                        if (keyValue.size == 1) {
                            val key = keyValue[0]
                            val value = ""
                            queryParams[key] = value
                        } else if (keyValue.size == 2) {
                            val key = keyValue[0]
                            val value = keyValue[1]
                            queryParams[key] = value
                        }
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
            return queryParams
        }
    }
}